import { computed, reactive, ref } from "vue";
import { asyncResult, isPromise, isFunction } from "../__use/util";
import { createStore, injectStore } from "ee-vuex";

/** Form 表单输入控件必备的 props */
export const formProps = {
  /** 挂载在原生表单控件上的 name 属性，提交表单时的字段名 */
  name: String,
  //modelValue
}

/** 都有 modelValue ，且 modelValue 变化时触发 change 事件 */
export const formMixin = injectStore({
  emits: {
    /** 选中项发生变化时触发
     * @param value - 当前选中的值
     * @param i - 组件当前实例
     */
    onChange: (value, i) => true,
  },
  props: {
    /** 挂载在原生表单控件上的 name 属性，提交表单时的字段名 */
    name: String,
    /** 表单提交的值 */
    modelValue(value, set) {
      set(value);
      this._onValue(value);
    }
  },
  methods: {
    /** modelValue 发生改变时的事件处理，默认回调 change 事件 */
    _onValue(value) { this.$emit("change", value, this); }
  }
})

/** 单个选项的对象定义
 * @typedef {object} optionObject
 * @property {string} group - 选项的分组展示标签。分组直到下一个选项 group 不为空且不一样为止
 * @property {string} label - 展示的标签
 * @property {*} value - 选项对应的值
 * @property {boolean} disabled - 选项是否可用
 * @property {options} children - 多级选项
 * @property {onSelect} onChange - 选项选中状态发生改变时触发
 * @property {onSelect} onChecked - 选项选中时触发
 */

/** 选项选中状态的回调
 * @callback onSelect
 * @param {optionObject} item - 选项
 * @param {boolean} checked - 选中状态
 * @param {any} value - 此时 modelValue 的值
 */

/** 单个选项
 * @typedef {optionObject | string} option
 */

/** 全部选项，使用对象时 key 是选项值，value 是选项对象或标签
 * @typedef {option[] | Object.<string, option>} options
 */

/** 选项字段对象映射
 * @typedef {object} optionMapper
 * @property {string | function(option): string} label - 字段映射
 * @property {string | function(option)} value - 字段映射
 * @property {string | function(option): boolean} disabled - 字段映射
 * @property {string | function(option): optionsProp} children - 字段映射
 */

/** 对象数组字段映射 (递归映射 children)
 * 
 * 当后端返回的对象不符合组件使用所需要的字段时，如下
 * 
 * ```
 * [
 *   { id: 1, name: '张三', state: 0 },
 *   { id: 2, name: '李四', state: 1 },
 * ]
 * ```
 * 
 * 解决方案有以下几种
 * 1. 类型转换：将类型转换成组件需要的格式
 *   - 缺点：转换耗时，多占用内存，每次转换代码冗余
 * 
 * 2. 字段名映射：传入对象 { label: 'name', value: 'id' }，将对象字段映射成组件需要字段
 *   - 缺点：读取耗时，disabled = (state == 0) 这样需要转换的字段无法直接映射，读取时映射不方便扩展
 * 
 * 3. 类型合并：传入对象 { label: (i) => i.name, value: (i) => i.id, disabled: (i) => i.state == 0 }
 *    将对象改变成 { id: 1, name: '张三', state: 0, label: '张三', value: 1, disabled: true }
 *   - 缺点：转换耗时，多占用内存，改变原始对象
 * 
 * 4. 自定义模板：v-for 自己循环对象生成模板
 *   - 缺点：手写模板代码多
 * 
 * 综上所述，每种方案都有缺点，最终给出以下方案
 * 
 * - 类型转换是用户行为，是否这样做由开发者自行决定
 * - 字段名映射和类型合并一起使用统一叫做映射，不合并对象，都以映射的方式渲染字段
 * - 兼容自定义模板
 * 
 * 这里提供一个类型转换方法，实现方式为 用户数据 + 映射，最终输出为合并的对象
 * 
 * 如果选项有 children ，则 children 也一并进行映射
 * @param {optionObject[]} options - 对象选项数组
 * @param {optionMapper} mapper - 映射
 * @returns {optionObject[]} - 合并的对象
 */
export function optionsMapping(options, mapper) {
  for (const option of options) {
    for (const key in mapper)
      combineMapping(option, key, mapper[key]);
    if (option.children)
      optionsMapping(option.children, mapper);
  }
  return options;
}

/** 映射一个选项 (不会映射 children)
 * @param {optionObject} option 
 * @param {optionMapper} mapper 
 */
export function optionMapping(option, mapper) {
  for (const key in mapper)
    combineMapping(option, key, mapper[key]);
}

/** 映射一个字段并赋值到选项对象里
 * @param {optionObject} o 
 * @param {string} name 
 * @param {string | function(option)} m 
 */
function combineMapping(o, name, m) {
  o[name] = m.constructor == String ? o[m] : m(o);
}

/** 选项单个字段的映射
 * @param {option} option - 选项
 * @param {string | function(option)} mapper - 映射器
 */
export function fieldMapping(option, mapper) {
  return mapper.constructor == String ? option[mapper] : mapper(option);
}

/** 将 options 整理成 option[] 类型
 * @param {options} ops 
 * @param {optionMapper} [mapper]
 * @returns {optionObject[]}
 */
export function arrange(ops, mapper) {
  /** @type {option[]} */
  const result = [];
  if (ops) {
    if (ops.constructor == Array) {
      // option[]
      for (const op of ops) {
        if (op.constructor == Object) {
          result.push(op);
          if (mapper)
            optionMapping(op, mapper);
          if (op.children)
            op.children = arrange(op.children, mapper);
        }
        else
          result.push({ label: op, value: op })
      }
      // group 数据现在只在第一项有就行，但是搜索时第一项可能会被过滤掉，所以要给每项都设置 group
      for (let i = 1; i < result.length; i++)
        if (result[i - 1].group && !result[i].group)
          result[i].group = result[i - 1].group;
    } else if (ops.constructor == Object) {
      // Object.<string, option> 类型的 options
      for (const key in ops) {
        /** @type {option} */
        let op;
        const value = ops[key];
        if (value == undefined)
          throw 'option 的值不能为空'
        if (value.constructor == Object) {
          op = value;
          op.value ??= key;
          if (mapper)
            optionMapping(op, mapper);
          // todo: 不修改原始数据的 children 要 clone 一个 op
          if (op.children)
            op.children = arrange(op.children, mapper);
        } else {
          op = {
            label: value,
            value: key,
          }
        }
        result.push(op);
      }
    }
  }
  // 防止字符串数组和对象新增加的对象是原始 js 对象而非 proxy 对象
  // 原始 js 对象赋值给组件的 data 时会变成 proxy 对象
  // js 原始和 proxy 对象比较将为 false，所以这里加上 reactive
  return reactive(result);
}

/**
 * @typedef {object} search
 * @property {number} [delay=1000] - 异步搜索选项的延迟时间(ms)
 * @property {undefined | Record<string, undefined | options>} [cache] - 缓存关键字的搜索结果
 * @property {function(string):Promise<options>} search - 根据关键字异步搜索选项的方法
 */

/** 
 * @callback filter
 * @param {string} keyword - 输入的筛选关键词
 * @param {optionObject[]} options - 正在筛选的选项
 * @returns {optionObject[] | Promise<optionObject[]>} - 筛选后满足筛选条件的选项
 */

/** @type {filter} */
function defaultLabelFilter(keyword, options) {
  return options.search(keyword, i => i.label)
}

/** 异步搜索选项
 * @param {string} keyword - 搜索关键字
 * @returns {Promise<options>} - 异步搜索的选项
 */
function searchOptions(keyword) {

}

/** 异步搜索选项的延迟时间(ms)
 * 
 * 连续快速输入关键词时，不进行搜索操作，节省性能和流量
 * 
 * 只有停止输入一个延迟时间后才真正开始搜索
 */
export let defaultSearchDelay = 1000;

export const optionsSimpleMixin = injectStore({
  props: {
    /** 选项组 props 可以赋值的选项
     * - type {options | function(): (options | Promise<options>)}
     * @type {optionObject[]}
     */
    options: {
      type: [Object, Array, Function],
      /** @type {optionObject[]} */
      default: undefined,
      get(value) {
        if (value)
          return arrange(value, this.$props.mapper)
      },
    },
    /** 选项对象字段映射
     * @type {optionMapper}
     */
    mapper: Object,
  },
})
export const optionsMixin = injectStore({
  data() {
    const store = createStore({
      _asyncFilter: {
        time: 0,
        timer: 0,
        keyword: "",
        promise: undefined,
        /** 缓存关键字的搜索结果
         * @type {Record<string, undefined | options>}
         */
        cache: {},
      },
      /** 经过筛选后的选项
       * @returns {optionObject[]}
       */
      filteredOptions() {
        const keyword = this.keyword;
        const options = this.options;
        // 没有关键词或者不需要筛选
        if (!keyword)
          return options || null;

        /** @type {filter} */
        let filterFunction;
        if (this.filter == true || !this.filter)
          filterFunction = defaultLabelFilter;
        else if (isFunction(this.filter)) {
          filterFunction = this.filter;
          // todo: 对于异步搜索结果，达到和 search 一样的效果
        }
        else {
          // 异步搜索对象
          const af = this._asyncFilter;
          if (af.keyword == keyword) {
            // console.log("命中关键词", keyword)
            return;
          }
          if (af.cache[keyword]) {
            // 正在搜索关键字 a ，关键字改为 ab 且有缓存，则应该立即显示缓存而不是加载
            af.promise = undefined;
            // console.log("命中缓存", keyword)
            return af.cache[keyword];
          }
          /** @type {search} */
          const search = this.filter;
          const delay = search.delay || defaultSearchDelay;
          const now = Date.now();
          const flag = now - af.time >= delay;
          af.time = now;
          af.keyword = keyword;
          if (flag) {
            const promise = new Promise((resolve) => {
              // 异步搜索选项连续快速输入时需间隔一定时间再调用搜索方法
              af.timer = setTimeout(() => {
                const keyword = af.keyword;
                search.search(keyword)
                  .then(i => {
                    const result = defaultLabelFilter(keyword, arrange(i, this.$props.mapper));
                    // 缓存结果
                    if (search.cache) {
                      // 缓存修改，会导致重新 get 值，但是会命中缓存
                      af.cache[keyword] = result;
                      search.cache = af.cache;
                    }
                    // 搜索返回了数据，但是已经开始了另一次搜索，则之前的搜索作废
                    if (af.keyword != keyword)
                      resolve(undefined);
                    else
                      resolve(result);
                  })
                  .finally(_ => {
                    if (af.keyword != keyword)
                      return;
                    af.timer = 0;
                    af.time = 0;
                    af.promise = undefined;
                  })
              }, delay);
            })
            af.promise = promise;
            return promise;
          }
          return;
        }

        return filterFunction(keyword, options);
      },
    }, { this: this })
    return {
      /** 正在异步加载数据 */
      _loading: false,

      _asyncFilter: {
        time: 0,
        timer: 0,
        keyword: "",
        promise: undefined,
        /** 缓存关键字的搜索结果
         * @type {Record<string, undefined | options>}
         */
        cache: {},
      },
      _asyncOptions: undefined,

      filteredOptions: computed({
        get: () => store.filteredOptions,
        set: (value) => store.filteredOptions = value,
      }),
    }
  },
  props: {
    /** 可以输入文字以筛选选项
     * 
     * 传入方法可以根据输入的内容，筛选选项是否满足筛选条件
     * @type {boolean | filter | search}
     */
    filter: [Boolean, Function, Object],
    /** 选项组 props 可以赋值的选项
     * @type {options | function(): (options | Promise<options>)}
     */
    options: {
      type: [Object, Array, Function],
      get(value) {
        if (this._asyncOptions) {
          if (this.keyword != undefined) {
            return asyncResult(this._asyncOptions()).finally(i => this._asyncOptions = undefined);
          } else {
            return;
          }
        }
        if (value)
          return arrange(value, this.$props.mapper)
      },
      set(value) {
        if (isFunction(value)) {
          this._asyncOptions = value;
          return [];
        }
      }
    },
    /** 选项对象字段映射
     * @type {optionMapper}
     */
    mapper: [Object],
  },
  computed: {
    /** 搜索的关键词
     * @returns {string}
     */
    keyword() { return undefined; },
    /** 当前是否正在加载选项
     * @returns {boolean}
     */
    loading() { return this._asyncFilter.promise || this._asyncOptions; },
  },
})

const iOption = {
  emits: {
    /** 一个选项状态改变时触发
     * @type {onSelect}
     */
    onSelect: (item, checked) => true,
  },
  data() {
    return {
      /** 每个选项被选中的缓存，用于比较选中状态变化以触发事件
       * @type {Record<any, boolean>}
       */
      _checked: {},
      /** 选项的选中状态在改变时触发 change 事件，但是一帧仅触发一次 */
      _dirty: true,
    }
  },
  computed: {
    /** @returns {optionObject[]} */
    _options() {
      return this.options || [];
    },
  },
  methods: {
    /** @override */
    _onValue() { },
    /** 是否选中了选项
     * @param {optionObject} item */
    isSelect(item) {
      const value = this._isSelect?.(item);
      if (!value != !this._checked[item.value]) {
        this._checked[item.value] = value;
        item.onChange?.(item, value, this.modelValue);
        this.$emit("select", item, value, this.modelValue);
        if (value)
          item.onChecked?.(item, value, this.modelValue);
        if (this._dirty) {
          this.$nextTick(() => {
            this.$emit("change", this.modelValue, this)
            this._dirty = true;
            this._onValueChange();
          });
          this._dirty = false;
        }
      }
      return value;
    },
    /** 选中的选项发生变化时回调 */
    _onValueChange() { },
  }
}

export const optionRadio = {
  extends: iOption,
  computed: {
    /** 选中的选项
     * @returns {optionObject} */
    selected() {
      if (this.modelValue)
        return this.options?.find(i => this._isSelect(i));
    }
  },
  methods: {
    /** @param {optionObject} item */
    _select(item) {
      this.modelValue = item.value;
    },
    /** 内部实现是否选中了选项
     * @param {optionObject} item */
    _isSelect(item) {
      return this.modelValue == item.value;
    },
    /** 是否不可选中选项
     * @param {optionObject} item */
    _isDisabled(item) {
      return item.disabled;
    },
  }
}

export const optionCheckbox = {
  extends: iOption,
  beforeMount() {
    if (!this.$props.modelValue)
      this.modelValue = [];
  },
  props: {
    /** 至少要选中的项数，选项过少则选中的不能取消 */
    min: [Number, String],
    /** 最多可选中的项数，选项过多则没选中的不能再选 */
    max: [Number, String],
  },
  computed: {
    /** 选中的选项
     * @returns {optionObject[]} */
    selected() {
      if (this.modelValue?.length)
        return this.options?.filter(i => this._isSelect(i));
    }
  },
  methods: {
    /** @param {optionObject} item */
    _select(item) {
      const toCheck = !this.modelValue.includes(item.value);
      if (toCheck)
        this.modelValue.push(item.value);
      else
        this.modelValue.remove(item.value);
      return true;
    },
    /** 是否选中了选项
     * @param {optionObject} item */
    _isSelect(item) {
      return this.modelValue.includes(item.value);
    },
    /** 是否不可选中选项
     * @param {optionObject} item */
    _isDisabled(item) {
      if (item.disabled)
        return true;
      const model = this.modelValue;
      // 超过最大选项数：没选中的不能再选
      if (this.max > 0 && model.length >= this.max && !model.includes(item.value))
        return true;
      if (this.min > 0) {
        // 低于最小选项数：选中的不能取消
        if (model.length < this.min)
          return model.includes(item.value);
        else
          // 选项数固定(最大最小选项数一样)：选够数量后，选中的都可以取消
          return this.max == this.min && !model.includes(item.value);
      }
    },
    /** 全选
     * @param {boolean} value - true: 全选 / false: 全不选
     */
    checkAll(value) {
      this.modelValue.length = 0;
      if (value)
        this.modelValue = this.options.map(i => i.value);
    },
    /** 没有全选则全选 / 已经全选则取消全选 */
    toggleAll() {
      this.checkAll(!(this.options.length == this.modelValue.length));
    },
    /** 反选 */
    reverseAll() {
      const m = this.modelValue;
      this.modelValue = this.options.filter(i => !m.includes(i.value));
    },
  },
}